﻿// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs;
using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs;

namespace Microsoft.CmdPal.UI.Helpers;

/// <summary>
/// Global error handler for Command Palette.
/// </summary>
internal sealed partial class GlobalErrorHandler
{
    // GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
    internal void Register(App app)
    {
        ArgumentNullException.ThrowIfNull(app);

        app.UnhandledException += App_UnhandledException;
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    }

    private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e)
    {
        // Exceptions thrown on the main UI thread are handled here.
        if (e.Exception != null)
        {
            HandleException(e.Exception, Context.MainThreadException);
        }
    }

    private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e)
    {
        // Exceptions thrown on background threads are handled here.
        if (e.ExceptionObject is Exception ex)
        {
            HandleException(ex, Context.AppDomainUnhandledException);
        }
    }

    private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
    {
        // This event is raised only when a faulted Task is garbage-collected
        // without its exception being observed. It is NOT raised immediately
        // when the Task faults; timing depends on GC finalization.
        e.SetObserved();
        HandleException(e.Exception, Context.UnobservedTaskException);
    }

    private static void HandleException(Exception ex, Context context)
    {
        Logger.LogError($"Unhandled exception detected ({context})", ex);

        if (context == Context.MainThreadException)
        {
            var error = DiagnosticsHelper.BuildExceptionMessage(ex, null);
            var report = $"""
                          This is an error report generated by Windows Command Palette.
                          If you are seeing this message, it means the application has encountered an unexpected issue.
                          You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
                          {error}
                          """;

            StoreReport(report, storeOnDesktop: false);

            string message;
            string caption;
            try
            {
                message = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Message");
                caption = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Caption");
            }
            catch
            {
                // The resource loader may not be available if the exception occurred during startup.
                // Fall back to hardcoded strings in that case.
                message = "Command Palette has encountered a fatal error and must close.";
                caption = "Command Palette - Fatal error";
            }

            PInvoke.MessageBox(
                HWND.Null,
                message,
                caption,
                MESSAGEBOX_STYLE.MB_ICONERROR);
        }
    }

    private static string? StoreReport(string report, bool storeOnDesktop)
    {
        // Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions
        // in case of crash storm.
        var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log");

        // Always store a copy in log directory, this way it is available for Bug Report Tool
        string? reportPath = null;
        if (Logger.CurrentVersionLogDirectoryPath != null)
        {
            reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath);
        }

        // Optionally store a copy on the desktop for user (in)convenience
        if (storeOnDesktop)
        {
            var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory));

            // show the desktop copy if both succeeded
            if (path != null)
            {
                reportPath = path;
            }
        }

        return reportPath;

        static string? Save(string reportContent, string reportFileName, Func<string> directory)
        {
            try
            {
                var logDirectory = directory();
                Directory.CreateDirectory(logDirectory);
                var reportFilePath = Path.Combine(logDirectory, reportFileName);
                File.WriteAllText(reportFilePath, reportContent);
                return reportFilePath;
            }
            catch (Exception ex)
            {
                Logger.LogError("Failed to store exception report", ex);
                return null;
            }
        }
    }

    private enum Context
    {
        Unknown = 0,
        MainThreadException,
        BackgroundThreadException,
        UnobservedTaskException,
        AppDomainUnhandledException,
    }
}
